package net.avcompris.binding.dom.impl;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;

import net.avcompris.binding.BindConfiguration;
import net.avcompris.binding.BindFunctions;
import net.avcompris.binding.Binder;
import net.avcompris.binding.dom.DomBindingException;
import net.avcompris.binding.impl.BinderXPathVariableResolver;
import net.avcompris.binding.impl.XPathFunction;
import net.avcompris.binding.impl.XPathFunctionResolver;

import org.jaxen.Context;
import org.jaxen.Function;
import org.jaxen.FunctionCallException;
import org.jaxen.FunctionContext;
import org.jaxen.JaxenException;
import org.jaxen.NamespaceContext;
import org.jaxen.UnresolvableException;
import org.jaxen.VariableContext;
import org.jaxen.dom.DOMXPath;
import org.w3c.dom.Node;

import com.avcompris.common.annotation.Nullable;
import com.avcompris.util.XMLUtils;
import com.google.common.collect.Iterables;

class JaxenDomBinderInvocationHandler extends
		AbstractDomBinderInvocationHandler {

	/**
	 * constructor.
	 */
	public JaxenDomBinderInvocationHandler(final Binder<Node> binder,
			final ClassLoader classLoader, final Class<?> clazz,
			final Node rootNode, final BindConfiguration configuration) {

		super(binder, classLoader, clazz, rootNode, configuration);

		// NAMESPACES

		final Map<String, String> uris = calcNamespaceMap(clazz);

		if (uris.isEmpty()) {

			nsContext = null;

		} else {

			nsContext = new NamespaceContext() {

				@Override
				public String translateNamespacePrefixToUri(final String prefix) {

					if (prefix == null) {

						return null;
					}

					return uris.get(prefix);
				}
			};
		}

		// FUNCTIONS

		final Collection<BindFunctions> functions = calcFunctions(clazz);

		this.functions = functions.isEmpty() ? null : functions;
	}

	private final NamespaceContext nsContext;

	private final Iterable<BindFunctions> functions;

	/**
	 * return the text content of a given node.
	 */
	@Override
	protected String getTextContent(@Nullable final Node node) {

		return XMLUtils.getTextContent(node);
	}

	private org.jaxen.XPath newXPath(@Nullable final Node node,
			final String expression,
			@Nullable final BinderXPathVariableResolver resolver)
			throws JaxenException {

		nonNullArgument(expression, "expression");

		final org.jaxen.XPath xpath = new DOMXPath(expression);

		if (nsContext != null) {

			xpath.setNamespaceContext(nsContext);
		}

		if (resolver != null) {

			xpath.setVariableContext(new VariableContext() {

				@Override
				@Nullable
				public Object getVariableValue(final String namespaceURI,
						final String prefix, final String localName)
						throws UnresolvableException {

					final QName qName = new QName(namespaceURI, localName,
							prefix);

					final Object value = resolver.resolveVariable(qName);

					if (value == null) {

						return value;

					} else if (value instanceof Node) {

						return value;

					} else {

						return value.toString();
					}
				}
			});

			if (functions != null) {

				final FunctionContext initialFunctionContext = xpath
						.getFunctionContext();

				final XPathFunctionResolver fnResolver = createDomFunctionResolver(
						node, functions);

				// fnResolver.resolveFunction(functionName, arity);

				final FunctionContext fnContext = new FunctionContext() {

					@Override
					public Function getFunction(final String namespaceURI,
							final String prefix, final String localName)
							throws UnresolvableException {

						final XPathFunction domFunction = fnResolver
								.resolveDomFunction(namespaceURI, localName);

						if (domFunction == null) {
							return initialFunctionContext.getFunction(
									namespaceURI, prefix, localName);
						}

						final Function function = new Function() {

							@SuppressWarnings("unchecked")
							@Override
							public Object call(
									final Context context,
									@SuppressWarnings("rawtypes") final List args)
									throws FunctionCallException {

								return domFunction
										.evaluate(args == null ? null
												: Iterables.toArray(args,
														Object.class));
							}
						};

						return function;
					}
				};

				xpath.setFunctionContext(fnContext);
			}
		}

		return xpath;
	}

	@Override
	protected Node evaluateToNode(final String expression,
			@Nullable final BinderXPathVariableResolver resolver,
			@Nullable final Node node) throws DomBindingException {

		nonNullArgument(expression, "expression");

		try {

			final org.jaxen.XPath xpath = newXPath(node, expression, resolver);

			return (Node) xpath.selectSingleNode(node);

		} catch (final JaxenException e) {
			throw new DomBindingException(expression, e);
		}
	}

	@Override
	protected double evaluateToNumber(final String expression,
			@Nullable final BinderXPathVariableResolver resolver,
			@Nullable final Node node) throws DomBindingException {

		nonNullArgument(expression, "expression");

		try {

			final org.jaxen.XPath xpath = newXPath(node, expression, resolver);

			return xpath.numberValueOf(node).doubleValue();

		} catch (final JaxenException e) {
			throw new DomBindingException(expression, e);
		}
	}

	@Override
	protected String evaluateToString(final String expression,
			@Nullable final BinderXPathVariableResolver resolver,
			@Nullable final Node node) throws DomBindingException {

		nonNullArgument(expression, "expression");

		try {

			final org.jaxen.XPath xpath = newXPath(node, expression, resolver);

			return xpath.stringValueOf(node);

		} catch (final JaxenException e) {
			throw new DomBindingException(expression, e);
		}
	}

	@Override
	protected boolean evaluateToBoolean(final String expression,
			@Nullable final BinderXPathVariableResolver resolver,
			@Nullable final Node node) throws DomBindingException {

		nonNullArgument(expression, "expression");

		try {

			final org.jaxen.XPath xpath = newXPath(node, expression, resolver);
			
			if (!xpath.booleanValueOf(node)) {
				
				return false;				
			}
			
			// Hack: Derogate from the boolean() function in the XPath spec.
			
			final String s= xpath.stringValueOf(node);
			
			if ("false".equals(s) || "FALSE".equals(s) || "yes".equals(s) || "YES".equals(s)) {
				return false;
			}
			
			return true;

		} catch (final JaxenException e) {
			throw new DomBindingException(expression, e);
		}
	}

	@Override
	protected List<Node> evaluateToList(final String expression,
			@Nullable final BinderXPathVariableResolver resolver,
			@Nullable final Node node) throws DomBindingException {

		nonNullArgument(expression, "expression");

		final List<?> list;

		try {

			final org.jaxen.XPath xpath = newXPath(node, expression, resolver);

			list = xpath.selectNodes(node);

		} catch (final JaxenException e) {
			throw new DomBindingException(expression, e);
		}

		final List<Node> nodes = new ArrayList<Node>();

		for (final Object n : list) {

			nodes.add((Node) n);
		}

		return nodes;
	}
}
